/****************************************************************************** * Copyright (C) Ultraleap, Inc. 2011-2021. * * * * Use subject to the terms of the Apache License 2.0 available at * * http://www.apache.org/licenses/LICENSE-2.0, or another agreement * * between Ultraleap and you, your company or other organization. * ******************************************************************************/ using Leap.Unity.Attributes; using System; using System.Collections.Generic; using UnityEngine; namespace Leap.Unity.Interaction { /// /// AnchorableBehaviours mix well with InteractionBehaviours you'd like to be able to /// pick up and place in specific locations, specified by other GameObjects with an /// Anchor component. /// public class AnchorableBehaviour : MonoBehaviour { [Disable] [SerializeField] [Tooltip("Whether or not this AnchorableBehaviour is actively attached to its anchor.")] private bool _isAttached = false; public bool isAttached { get { return _isAttached; } set { if (_isAttached != value) { if (value == true) { if (_anchor != null) { _isAttached = value; _anchor.NotifyAttached(this); OnAttachedToAnchor.Invoke(); } else { Debug.LogWarning("Tried to attach an anchorable behaviour, but it has no assigned anchor.", this.gameObject); } } else { _isAttached = false; _isLockedToAnchor = false; _isRotationLockedToAnchor = false; OnDetachedFromAnchor.Invoke(); _anchor.NotifyDetached(this); _hasTargetPositionLastUpdate = false; _hasTargetRotationLastUpdate = false; // TODO: A more robust gravity fix. if (_reactivateGravityOnDetach) { if (interactionBehaviour != null) { interactionBehaviour.rigidbody.useGravity = true; } _reactivateGravityOnDetach = false; } } } } } [Tooltip("The current anchor of this AnchorableBehaviour.")] [OnEditorChange("anchor"), SerializeField] private Anchor _anchor; public Anchor anchor { get { return _anchor; } set { if (_anchor != value) { if (IsValidAnchor(value)) { if (_anchor != null && _isAttached) { OnDetachedFromAnchor.Invoke(); _anchor.NotifyDetached(this); } _isLockedToAnchor = false; _isRotationLockedToAnchor = false; _anchor = value; _hasTargetPositionLastUpdate = false; _hasTargetRotationLastUpdate = false; if (_anchor != null) { isAttached = true; } else { isAttached = false; } } else { Debug.LogWarning("The '" + value.name + "' anchor is not in " + this.name + "'s anchor group.", this.gameObject); } } } } [Tooltip("The anchor group for this AnchorableBehaviour. If set to null, all Anchors " + "will be valid anchors for this object.")] [OnEditorChange("anchorGroup"), SerializeField] private AnchorGroup _anchorGroup; public AnchorGroup anchorGroup { get { return _anchorGroup; } set { if (_anchorGroup != null) { _anchorGroup.NotifyAnchorableObjectRemoved(this); } _anchorGroup = value; if (anchor != null && !_anchorGroup.Contains(anchor)) { anchor = null; Debug.LogWarning(this.name + "'s anchor is not within its anchorGroup (setting it to null).", this.gameObject); } if (_anchorGroup != null) { _anchorGroup.NotifyAnchorableObjectAdded(this); } } } [Header("Attachment")] [Tooltip("Anchors beyond this range are ignored as possible anchors for this object.")] public float maxAnchorRange = 0.3F; [Tooltip("Only allowed when an InteractionBehaviour is attached to this object. If enabled, this " + "object's Attach() method or its variants will weigh its velocity towards an anchor along " + "with its proximity when seeking an anchor to attach to.")] [DisableIf("_interactionBehaviourIsNull", true)] public bool useTrajectory = true; [Tooltip("The fraction of the maximum anchor range to use as the effective max range when " + "useTrajectory is enabled, but the object attempts to find an anchor without any " + "velocity.")] [SerializeField] [Range(0.01F, 1F)] private float _motionlessRangeFraction = 0.40F; [SerializeField, Disable] private float _maxMotionlessRange; [Tooltip("The maximum angle this object's trajectory can be away from an anchor to consider it as " + "an anchor to attach to.")] [SerializeField] [Range(20F, 90F)] private float _maxAttachmentAngle = 60F; /// Calculated via _maxAttachmentAngle. private float _minAttachmentDotProduct; [Tooltip("Always attach an anchor if there is one within this distance, regardless " + "of trajectory.")] [SerializeField] [MinValue(0f)] private float _alwaysAttachDistance = 0f; [Header("Motion")] [Tooltip("Should the object move instantly to the anchor position?")] public bool lockToAnchor = false; [Tooltip("Should the object move smoothly towards the anchor at first, but lock to it once it reaches the anchor? " + "Note: Disabling the AnchorableBehaviour will stop the object from moving towards its anchor, and will " + "'release' it from the anchor, so that on re-enable the object will smoothly move to the anchor again.")] [DisableIf("lockToAnchor", isEqualTo: true)] public bool lockWhenAttached = true; [Tooltip("While this object is moving smoothly towards its anchor, should it also inherit the motion of the " + "anchor itself if the anchor is not stationary? Otherwise, the anchor might be able to run away from this " + "AnchorableBehaviour and prevent it from actually getting to the anchor.")] [DisableIf("lockToAnchor", isEqualTo: true)] public bool matchAnchorMotionWhileAttaching = true; [Tooltip("How fast should the object move towards its target position? Higher values are faster.")] [DisableIf("lockToAnchor", isEqualTo: true)] [Range(0, 100F)] public float anchorLerpCoeffPerSec = 20F; [Header("Rotation")] [Tooltip("Should the object also rotate to match its anchor's rotation? If checked, motion settings applied " + "to how the anchor translates will also apply to how it rotates.")] public bool anchorRotation = false; [Header("Interaction")] [Tooltip("Additional features are enabled when this GameObject also has an InteractionBehaviour component.")] [Disable] public InteractionBehaviour interactionBehaviour; [SerializeField, HideInInspector] private bool _interactionBehaviourIsNull = true; [Tooltip("If the InteractionBehaviour is set, objects will automatically detach from their anchor when grasped.")] [Disable] public bool detachWhenGrasped = true; [Tooltip("Should the AnchorableBehaviour automatically try to anchor itself when a grasp ends? If useTrajectory is enabled, " + "this object will automatically attempt to attach to the nearest valid anchor that is in the direction of its trajectory, " + "otherwise it will simply attempt to attach to its nearest valid anchor.")] [SerializeField] [OnEditorChange("tryAnchorNearestOnGraspEnd")] private bool _tryAnchorNearestOnGraspEnd = true; public bool tryAnchorNearestOnGraspEnd { get { return _tryAnchorNearestOnGraspEnd; } set { if (interactionBehaviour != null) { // Prevent duplicate subscription. interactionBehaviour.OnGraspEnd -= tryToAnchorOnGraspEnd; } _tryAnchorNearestOnGraspEnd = value; if (interactionBehaviour != null && _tryAnchorNearestOnGraspEnd) { interactionBehaviour.OnGraspEnd += tryToAnchorOnGraspEnd; } } } [Tooltip("Should the object pull away from its anchor and reach towards the user's hand when the user's hand is nearby?")] public bool isAttractedByHand = false; [Tooltip("If the object is attracted to hands, how far should the object be allowed to pull away from its anchor " + "towards a nearby InteractionHand? Value is in Unity distance units, WORLD space.")] public float maxAttractionReach = 0.1F; [Tooltip("This curve converts the distance of the hand (X axis) to the desired attraction reach distance for the object (Y axis). " + "The evaluated value is clamped between 0 and 1, and then scaled by maxAttractionReach.")] public AnimationCurve attractionReachByDistance; private Anchor _preferredAnchor = null; /// /// Gets the anchor this AnchorableBehaviour would most prefer to attach to. /// This value is refreshed every Update() during which the AnchorableBehaviour /// has no anchor or is detached from its current anchor. /// public Anchor preferredAnchor { get { return _preferredAnchor; } } #region Events /// /// Called when this AnchorableBehaviour attaches to an Anchor. /// public Action OnAttachedToAnchor = () => { }; /// /// Called when this AnchorableBehaviour locks to an Anchor. /// public Action OnLockedToAnchor = () => { }; /// /// Called when this AnchorableBehaviour detaches from an Anchor. /// public Action OnDetachedFromAnchor = () => { }; /// /// Called during every Update() in which this AnchorableBehaviour is attached to an Anchor. /// public Action WhileAttachedToAnchor = () => { }; /// /// Called during every Update() in which this AnchorableBehaviour is locked to an Anchor. /// public Action WhileLockedToAnchor = () => { }; /// /// Called just after this anchorable behaviour's InteractionBehaviour OnObjectGraspEnd for /// this anchor. This callback will never fire if tryAttachAnchorOnGraspEnd is not enabled. /// /// If tryAttachAnchorOnGraspEnd is enabled, the anchor will be attached to /// an anchor only if its preferredAnchor property is non-null; otherwise, the /// attempt to anchor failed. /// public Action OnPostTryAnchorOnGraspEnd = () => { }; #endregion private bool _isLockedToAnchor = false; private Vector3 _offsetTowardsHand = Vector3.zero; private Vector3 _targetPositionLastUpdate = Vector3.zero; private bool _hasTargetPositionLastUpdate = false; private bool _isRotationLockedToAnchor = false; private Quaternion _targetRotationLastUpdate = Quaternion.identity; private bool _hasTargetRotationLastUpdate = false; void OnValidate() { refreshInteractionBehaviour(); refreshInspectorConveniences(); } void Reset() { refreshInteractionBehaviour(); } void Awake() { refreshInteractionBehaviour(); refreshInspectorConveniences(); if (anchorGroup != null) { anchorGroup.NotifyAnchorableObjectAdded(this); } if (interactionBehaviour != null) { interactionBehaviour.OnGraspBegin += detachAnchorOnGraspBegin; if (_tryAnchorNearestOnGraspEnd) { interactionBehaviour.OnGraspEnd += tryToAnchorOnGraspEnd; } } initUnityEvents(); } void Start() { if (anchor != null && _isAttached) { anchor.NotifyAttached(this); OnAttachedToAnchor(); } } private bool _reactivateGravityOnDetach = false; void Update() { updateAttractionToHand(); if (anchor != null && isAttached) { if (interactionBehaviour != null && interactionBehaviour.rigidbody.useGravity) { // TODO: This is a temporary fix for gravity to be fixed in a future IE PR. // The proper solution involves switching the behaviour to FixedUpdate and more // intelligently communicating with the attached InteractionBehaviour. interactionBehaviour.rigidbody.useGravity = false; _reactivateGravityOnDetach = true; } updateAnchorAttachment(); if (anchorRotation) { updateAnchorAttachmentRotation(); } WhileAttachedToAnchor.Invoke(); if (_isLockedToAnchor) { WhileLockedToAnchor.Invoke(); } } updateAnchorPreference(); } void OnDisable() { if (!this.enabled) { Detach(); } // Make sure we don't leave dangling anchor-preference state. endAnchorPreference(); } void OnDestroy() { if (interactionBehaviour != null) { interactionBehaviour.OnGraspBegin -= detachAnchorOnGraspBegin; interactionBehaviour.OnGraspEnd -= tryToAnchorOnGraspEnd; } // Make sure we don't leave dangling anchor-preference state. endAnchorPreference(); } private void refreshInspectorConveniences() { _minAttachmentDotProduct = Mathf.Cos(_maxAttachmentAngle * Mathf.Deg2Rad); _maxMotionlessRange = maxAnchorRange * _motionlessRangeFraction; } private void refreshInteractionBehaviour() { interactionBehaviour = GetComponent(); _interactionBehaviourIsNull = interactionBehaviour == null; detachWhenGrasped = !_interactionBehaviourIsNull; if (_interactionBehaviourIsNull) { useTrajectory = false; } } /// /// Detaches this Anchorable object from its anchor. The anchor reference /// remains unchanged. Call TryAttach() to re-attach to this object's assigned anchor. /// public void Detach() { isAttached = false; } /// /// Returns whether the argument anchor is an acceptable anchor for this anchorable /// object; that is, whether the argument Anchor is within this behaviour's AnchorGroup /// if it has one, or if this behaviour has no AnchorGroup, returns true. /// public bool IsValidAnchor(Anchor anchor) { if (anchor == null) { return true; } if (this.anchorGroup != null) { return this.anchorGroup.Contains(anchor); } else { return true; } } /// /// Returns whether the specified anchor is within attachment range of this Anchorable object. /// public bool IsWithinRange(Anchor anchor) { return (this.transform.position - anchor.transform.position).sqrMagnitude < maxAnchorRange * maxAnchorRange; } /// /// Attempts to find and return the best anchor for this anchorable object to attach to /// based on its current configuration. If useTrajectory is enabled, the object will /// consider anchor proximity as well as its own trajectory towards a particular anchor, /// and may return null if the object is moving away from all of its possible anchors. /// Otherwise, the object will simply return the nearest valid anchor, or null if there /// is no valid anchor nearby. /// /// This method is called every Update() automatically by anchorable objects, and its /// result is stored in preferredAnchor. Only call this if you need a new calculation. /// public Anchor FindPreferredAnchor() { if (!useTrajectory) { // Simply try to attach to the nearest valid anchor. return GetNearestValidAnchor(); } else { // Pick the nearby valid anchor with the highest score, based on proximity and trajectory. Anchor optimalAnchor = null; float optimalScore = 0F; Anchor testAnchor = null; float testScore = 0F; foreach (var anchor in GetNearbyValidAnchors()) { testAnchor = anchor; testScore = getAnchorScore(anchor); // Scores of 0 mark ineligible anchors. if (testScore == 0F) { continue; } if (testScore > optimalScore) { optimalAnchor = testAnchor; optimalScore = testScore; } } return optimalAnchor; } } private List _nearbyAnchorsBuffer = new List(); /// /// Returns all anchors within the max anchor range of this anchorable object. If this /// anchorable object has its anchorGroup property set, only anchors within that AnchorGroup /// will be returned. By default, this method will only return anchors that have space for /// an object to attach to it. /// /// Warning: This method checks squared-distance for all anchors in teh scene if this /// AnchorableBehaviour has no AnchorGroup. /// public List GetNearbyValidAnchors(bool requireAnchorHasSpace = true, bool requireAnchorActiveAndEnabled = true) { HashSet anchorsToCheck; if (this.anchorGroup == null) { anchorsToCheck = Anchor.allAnchors; } else { anchorsToCheck = this.anchorGroup.anchors; } _nearbyAnchorsBuffer.Clear(); foreach (var anchor in anchorsToCheck) { if ((requireAnchorHasSpace && (!anchor.allowMultipleObjects && anchor.anchoredObjects.Count != 0)) || (requireAnchorActiveAndEnabled && !anchor.isActiveAndEnabled)) { continue; } if ((anchor.transform.position - this.transform.position).sqrMagnitude <= maxAnchorRange * maxAnchorRange) { _nearbyAnchorsBuffer.Add(anchor); } } return _nearbyAnchorsBuffer; } /// /// Returns the nearest valid anchor to this Anchorable object. If this anchorable object has its /// anchorGroup property set, all anchors within that AnchorGroup are valid to be this object's /// anchor. If there is no valid anchor within range, returns null. By default, this method will /// only return anchors that are within the max anchor range of this object and that have space for /// an object to attach to it. /// /// Warning: This method checks squared-distance for all anchors in the scene if this AnchorableBehaviour /// has no AnchorGroup. /// public Anchor GetNearestValidAnchor(bool requireWithinRange = true, bool requireAnchorHasSpace = true, bool requireAnchorActiveAndEnabled = true) { HashSet anchorsToCheck; if (this.anchorGroup == null) { anchorsToCheck = Anchor.allAnchors; } else { anchorsToCheck = this.anchorGroup.anchors; } Anchor closestAnchor = null; float closestDistSqrd = float.PositiveInfinity; foreach (var testAnchor in anchorsToCheck) { if (requireAnchorHasSpace) { bool anchorHasSpace = testAnchor.anchoredObjects.Count == 0 || testAnchor.allowMultipleObjects; if (!anchorHasSpace) { // Skip the anchor for consideration. continue; } } if (requireAnchorActiveAndEnabled && !testAnchor.isActiveAndEnabled) { // Skip the anchor for consideration. continue; } float testDistanceSqrd = (testAnchor.transform.position - this.transform.position).sqrMagnitude; if (testDistanceSqrd < closestDistSqrd) { closestAnchor = testAnchor; closestDistSqrd = testDistanceSqrd; } } if (!requireWithinRange || closestDistSqrd < maxAnchorRange * maxAnchorRange) { return closestAnchor; } else { return null; } } /// /// Attempts to attach to this Anchorable object's currently specified anchor. /// The attempt may fail if this anchor is out of range. Optionally, the range /// requirement can be ignored. /// public bool TryAttach(bool ignoreRange = false) { if (anchor != null && (ignoreRange || IsWithinRange(anchor))) { isAttached = true; return true; } else { return false; } } /// /// Attempts to find and attach this anchorable object to the nearest valid anchor, or the /// most optimal nearby anchor based on proximity and the object's trajectory if useTrajectory /// is enabled. /// public bool TryAttachToNearestAnchor() { Anchor preferredAnchor = FindPreferredAnchor(); if (preferredAnchor != null) { _preferredAnchor = preferredAnchor; anchor = preferredAnchor; isAttached = true; return true; } return false; } /// Score an anchor based on its proximity and this object's trajectory relative to it. private float getAnchorScore(Anchor anchor) { return GetAnchorScore(this.interactionBehaviour.rigidbody.position, this.interactionBehaviour.rigidbody.velocity, anchor.transform.position, maxAnchorRange, _maxMotionlessRange, _minAttachmentDotProduct, _alwaysAttachDistance); } /// /// Calculates and returns a score from 0 (non-valid anchor) to 1 (ideal anchor) based on /// the argument configuration, using an anchorable object's position and velocity, an /// anchor position, and distance/angle settings. A score of zero indicates an invalid /// anchor no matter what; a non-zero score indicates a possible anchor, with more optimal /// anchors receiving a score closer to 1. /// public static float GetAnchorScore(Vector3 anchObjPos, Vector3 anchObjVel, Vector3 anchorPos, float maxDistance, float nonDirectedMaxDistance, float minAngleProduct, float alwaysAttachDistance = 0f) { // Calculated a "directedness" heuristic for determining whether the user is throwing or releasing without directed motion. float directedness = anchObjVel.magnitude.Map(0.20F, 1F, 0F, 1F); float effMaxDistance = directedness.Map(0F, 1F, nonDirectedMaxDistance, maxDistance); Vector3 effPos = Utils.Map(Mathf.Sqrt(Mathf.Sqrt(directedness)), 0f, 1f, anchObjPos, (anchObjPos - anchObjVel.normalized * effMaxDistance * 0.30f)); float distanceSqrd = (anchorPos - effPos).sqrMagnitude; float distanceScore; if (distanceSqrd > effMaxDistance * effMaxDistance) { distanceScore = 0F; } else { distanceScore = distanceSqrd.Map(0F, effMaxDistance * effMaxDistance, 1F, 0F); } float angleScore; float dotProduct = Vector3.Dot(anchObjVel.normalized, (anchorPos - effPos).normalized); // Angular score only factors in based on how directed the motion of the object is. dotProduct = Mathf.Lerp(1F, dotProduct, directedness); angleScore = dotProduct.Map(minAngleProduct, 1f, 0f, 1f); angleScore *= angleScore; // Support an "always-attach distance" within which only distanceScore matters float semiDistanceSqrd = (anchorPos - Vector3.Lerp(anchObjPos, effPos, 0.5f)).sqrMagnitude; float useAlwaysAttachDistanceAmount = semiDistanceSqrd.Map(0f, Mathf.Max(0.0001f, (0.25f * alwaysAttachDistance * alwaysAttachDistance)), 1f, 0f); angleScore = useAlwaysAttachDistanceAmount.Map(0f, 1f, angleScore, 1f); return distanceScore * angleScore; } private void updateAttractionToHand() { if (interactionBehaviour == null || anchor == null || !isAttractedByHand) { if (_offsetTowardsHand != Vector3.zero) { _offsetTowardsHand = Vector3.Lerp(_offsetTowardsHand, Vector3.zero, 5F * Time.deltaTime); } return; } float reachTargetAmount = 0F; Vector3 towardsHand = Vector3.zero; if (interactionBehaviour.isHovered) { Vector3 hoverTarget = Vector3.zero; InteractionController hoveringController = interactionBehaviour.closestHoveringController; if (hoveringController is InteractionHand) { Hand hoveringHand = interactionBehaviour.closestHoveringHand; hoverTarget = hoveringHand.PalmPosition.ToVector3(); } else { hoverTarget = hoveringController.hoverPoint; } reachTargetAmount = Mathf.Clamp01(attractionReachByDistance.Evaluate( Vector3.Distance(hoverTarget, anchor.transform.position))); towardsHand = hoverTarget - anchor.transform.position; } Vector3 targetOffsetTowardsHand = towardsHand * maxAttractionReach * reachTargetAmount; _offsetTowardsHand = Vector3.Lerp(_offsetTowardsHand, targetOffsetTowardsHand, 5 * Time.deltaTime); } private void updateAnchorAttachment() { // Initialize position. Vector3 finalPosition; if (interactionBehaviour != null) { finalPosition = interactionBehaviour.rigidbody.position; } else { finalPosition = this.transform.position; } // Update position based on anchor state. Vector3 targetPosition = anchor.transform.position; if (lockToAnchor) { // In this state, we are simply locked directly to the anchor. finalPosition = targetPosition + _offsetTowardsHand; // Reset anchor position storage; it can't be updated from this state. _hasTargetPositionLastUpdate = false; } else if (lockWhenAttached) { if (_isLockedToAnchor) { // In this state, we are already attached to the anchor. finalPosition = targetPosition + _offsetTowardsHand; // Reset anchor position storage; it can't be updated from this state. _hasTargetPositionLastUpdate = false; } else { // Undo any "reach towards hand" offset. finalPosition -= _offsetTowardsHand; // If desired, automatically correct for the anchor itself moving while attempting to return to it. if (matchAnchorMotionWhileAttaching && this.transform.parent != anchor.transform) { if (_hasTargetPositionLastUpdate) { finalPosition += (targetPosition - _targetPositionLastUpdate); } _targetPositionLastUpdate = targetPosition; _hasTargetPositionLastUpdate = true; } // Lerp towards the anchor. finalPosition = Vector3.Lerp(finalPosition, targetPosition, anchorLerpCoeffPerSec * Time.deltaTime); if (Vector3.Distance(finalPosition, targetPosition) < 0.001F) { _isLockedToAnchor = true; } // Redo any "reach toward hand" offset. finalPosition += _offsetTowardsHand; } } // Set final position. if (interactionBehaviour != null) { interactionBehaviour.rigidbody.position = finalPosition; this.transform.position = finalPosition; } else { this.transform.position = finalPosition; } } private void updateAnchorAttachmentRotation() { // Initialize rotation. Quaternion finalRotation; if (interactionBehaviour != null) { finalRotation = interactionBehaviour.rigidbody.rotation; } else { finalRotation = this.transform.rotation; } // Update rotation based on anchor state. Quaternion targetRotation = anchor.transform.rotation; if (lockToAnchor) { // In this state, we are simply locked directly to the anchor. finalRotation = targetRotation; // Reset anchor rotation storage; it can't be updated from this state. _hasTargetPositionLastUpdate = false; } else if (lockWhenAttached) { if (_isRotationLockedToAnchor) { // In this state, we are already attached to the anchor. finalRotation = targetRotation; // Reset anchor rotation storage; it can't be updated from this state. _hasTargetRotationLastUpdate = false; } else { // If desired, automatically correct for the anchor itself moving while attempting to return to it. if (matchAnchorMotionWhileAttaching && this.transform.parent != anchor.transform) { if (_hasTargetRotationLastUpdate) { finalRotation = (Quaternion.Inverse(_targetRotationLastUpdate) * targetRotation) * finalRotation; } _targetRotationLastUpdate = targetRotation; _hasTargetRotationLastUpdate = true; } // Slerp towards the anchor rotation. finalRotation = Quaternion.Slerp(finalRotation, targetRotation, anchorLerpCoeffPerSec * 0.8F * Time.deltaTime); if (Quaternion.Angle(targetRotation, finalRotation) < 2F) { _isRotationLockedToAnchor = true; } } } // Set final rotation. if (interactionBehaviour != null) { interactionBehaviour.rigidbody.rotation = finalRotation; this.transform.rotation = finalRotation; } else { this.transform.rotation = finalRotation; } } private void updateAnchorPreference() { Anchor newPreferredAnchor; if (!isAttached) { newPreferredAnchor = FindPreferredAnchor(); } else { newPreferredAnchor = null; } if (_preferredAnchor != newPreferredAnchor) { if (_preferredAnchor != null) { _preferredAnchor.NotifyEndAnchorPreference(this); } _preferredAnchor = newPreferredAnchor; if (_preferredAnchor != null) { _preferredAnchor.NotifyAnchorPreference(this); } } } private void endAnchorPreference() { if (_preferredAnchor != null) { _preferredAnchor.NotifyEndAnchorPreference(this); _preferredAnchor = null; } } private void detachAnchorOnGraspBegin() { Detach(); } private void tryToAnchorOnGraspEnd() { TryAttachToNearestAnchor(); OnPostTryAnchorOnGraspEnd(); } #region Unity Events (Internal) #pragma warning disable 0649 [SerializeField] private EnumEventTable _eventTable; #pragma warning restore 0649 public enum EventType { OnAttachedToAnchor = 100, OnLockedToAnchor = 105, OnDetachedFromAnchor = 110, WhileAttachedToAnchor = 120, WhileLockedToAnchor = 125, OnPostTryAnchorOnGraspEnd = 130 } private void initUnityEvents() { // If the interaction component is added at runtime, _eventTable won't have been // constructed yet. if (_eventTable == null) { _eventTable = new EnumEventTable(); } setupCallback(ref OnAttachedToAnchor, EventType.OnAttachedToAnchor); setupCallback(ref OnLockedToAnchor, EventType.OnLockedToAnchor); setupCallback(ref OnDetachedFromAnchor, EventType.OnDetachedFromAnchor); setupCallback(ref WhileAttachedToAnchor, EventType.WhileAttachedToAnchor); setupCallback(ref WhileLockedToAnchor, EventType.WhileLockedToAnchor); setupCallback(ref OnPostTryAnchorOnGraspEnd, EventType.OnPostTryAnchorOnGraspEnd); } private void setupCallback(ref Action action, EventType type) { if (_eventTable.HasUnityEvent((int)type)) { action += () => _eventTable.Invoke((int)type); } else { action += () => { }; } } #endregion } }